React 稳定版常用 API & Hooks 总表(不含实验特性)
| API / Hook | 作用 | 基本用法 | 常见场景 | 面试高频点 / 注意事项 |
|---|---|---|---|---|
createElement | 创建 React 元素(JSX 底层) | React.createElement('div', null, 'Hello') | JSX 编译后底层实现 | JSX 本质不是 HTML,而是 createElement |
Fragment | 避免额外 DOM 包裹 | <></> | 返回多个元素 | 不生成真实 DOM |
StrictMode | 开发模式检查潜在问题 | <React.StrictMode> | 检查副作用 | 开发环境会触发额外 render |
Suspense | 处理异步加载 fallback | <Suspense fallback={<Loading />}> | 懒加载、数据加载 | 常配合 lazy |
lazy | 动态导入组件 | const A = lazy(() => import('./A')) | 路由拆包 | 必须配合 Suspense |
memo | 缓存组件,避免重复渲染 | memo(Component) | 性能优化 | 浅比较 props |
forwardRef | 父组件传 ref 给子组件 | forwardRef((props, ref)=>{}) | 获取子组件 DOM | 常搭配 useImperativeHandle |
createContext | 创建上下文 | const Ctx = createContext() | 跨层级传值 | 避免 props drilling |
| createRef | 创建 ref(类组件常用) | this.ref = createRef() | DOM 操作 | 函数组件通常用 useRef |
基础 Hooks
| Hook | 作用 | 基本用法 | 常见场景 | 面试高频点 / 注意事项 |
|---|---|---|---|---|
useState | 状态管理 | const [count, setCount] = useState(0) | 表单、计数器 | 异步更新、批处理 |
useEffect | 副作用处理 | useEffect(()=>{}, []) | 请求、监听、定时器 | 清理函数防内存泄漏 |
useContext | 获取 Context 数据 | useContext(MyContext) | 全局主题、用户信息 | Provider value 改变会触发消费组件更新 |
useReducer | 复杂状态管理 | useReducer(reducer, init) | 多状态逻辑 | 类似 Redux |
useRef | 持久引用,不触发渲染 | const ref = useRef() | DOM、缓存变量 | 改变 .current 不 rerender |
性能优化 Hooks
| Hook | 作用 | 基本用法 | 常见场景 | 面试高频点 / 注意事项 |
|---|---|---|---|---|
useMemo | 缓存计算结果 | useMemo(()=>fn(), [deps]) | 大计算 | 缓存值 |
useCallback | 缓存函数引用 | useCallback(()=>{}, [deps]) | 子组件 props | 缓存函数 |
useTransition | 标记低优先级更新 | const [isPending, startTransition] = useTransition() | 搜索、过滤 | 提升交互流畅度 |
useDeferredValue | 延迟值更新 | const deferred = useDeferredValue(value) | 输入搜索优化 | 类似防抖但不是防抖 |
DOM / 生命周期 Hooks
| Hook | 作用 | 基本用法 | 常见场景 | 面试高频点 / 注意事项 |
|---|---|---|---|---|
useLayoutEffect | DOM 更新后、绘制前执行 | useLayoutEffect(()=>{}) | 测量 DOM | 会阻塞绘制 |
useImperativeHandle | 自定义 ref 暴露内容 | useImperativeHandle(ref, ()=>({...})) | 暴露子组件方法 | 配合 forwardRef |
useId | 生成稳定唯一 ID | const id = useId() | 表单 label/input | SSR 防止 hydration mismatch |
useSyncExternalStore | 外部状态同步 | useSyncExternalStore(subscribe, getSnapshot) | Redux/Zustand | React 18 外部状态标准 |
useInsertionEffect | 样式插入前执行 | CSS-in-JS 库使用 | styled-components | 很少业务直接用 |
React DOM API
| API | 作用 | 基本用法 | 注意事项 |
|---|---|---|---|
createRoot | React 18 新渲染入口 | createRoot(root).render(<App />) | 替代 ReactDOM.render |
hydrateRoot | SSR hydration | hydrateRoot(el, <App />) | Next.js SSR 常见 |
flushSync | 强制同步更新 | flushSync(()=>setX()) | 谨慎使用 |
Hooks 使用规则(面试必考)
| 规则 | 说明 |
|---|---|
| 只能在函数组件或自定义 Hook 顶层调用 | 不能在循环/条件/嵌套函数里 |
| 必须保持调用顺序一致 | React 靠顺序记录 Hook |
自定义 Hook 必须 use 开头 | 规范 + lint 检查 |
高频对比(面试非常常见)
| 对比 | 核心区别 |
|---|---|
useState vs useRef | state 改变会触发渲染;ref 不触发 |
useEffect vs useLayoutEffect | effect 异步;layoutEffect 同步阻塞 |
useMemo vs useCallback | memo 缓存值;callback 缓存函数 |
Context vs Redux | Context 传值;Redux 是更强的状态管理 |
createRef vs useRef | createRef 每次新建;useRef 持久 |
一句话记忆(适合面试口语)
面试官最爱问的坑点
useEffect 会执行两次?因为 React StrictMode 开发模式故意检查副作用。
setState 不立即更新?因为批处理 + 调度机制。
useMemo/useCallback?因为本身也有性能成本。
ref 为什么不触发更新?因为它不是响应式状态。
最推荐你的记忆顺序(前端面试)
先背:
useState → useEffect → useRef → useMemo → useCallback → useContext → useReducer
再背:
forwardRef → useImperativeHandle → useTransition → useSyncExternalStore
对于大部分前端面试,这套已经覆盖 90%+ React 问题。
useState 和普通变量(例如 let count = 0)的核心区别是什么?为什么 React 不直接用普通变量管理 UI?useState 和普通变量最核心的区别有两个:
1let count = 0;2count++;即使值变了,React 并不知道这个变化,所以 UI 不会自动更新。
useState 会被 React 保存,并在更新时触发 rerenderxxxxxxxxxx21const [count, setCount] = useState(0);2setCount(count + 1);调用 setCount 后:
xxxxxxxxxx31function Counter() {2 let count = 0;3}每次 render 都重新从 0 开始。
而 useState 的值不会因为函数重新执行而丢失,因为 React 是按 Hook 链表(或索引)保存状态。
一句话总结: 普通变量只能存值;useState 不仅存值,还能通知 React 更新界面。
Standard Answer (English)
The core difference between useState and normal variables is:
xxxxxxxxxx21let count = 0;2count++;Even if the value changes, React does not track it, so the UI will not update.
useState is tracked by Reactxxxxxxxxxx21const [count, setCount] = useState(0);2setCount(count + 1);When setCount is called:
Function components re-run entirely, so local variables are recreated.
useState persists because React stores it outside the function body.
面试官为什么问这个?
因为这是在考你是否真正理解:
实战场景
错误写法:
xxxxxxxxxx21let loading = false;2loading = true;页面不会更新。
正确写法:
xxxxxxxxxx21const [loading, setLoading] = useState(false);2setLoading(true);常见误区
错误:“useState 就是一个普通变量升级版”
正确:“useState 本质是 React 的状态调度系统的一部分”
面试口语版(中文)
useState 和普通变量最大的区别是,普通变量变化不会让 React 重新渲染,而且组件重新执行时变量会重置。useState 的状态是 React 保存的,调用 setter 后 React 会触发更新,所以 UI 能同步变化。
Interview Spoken Answer (English)
The biggest difference is that normal variables are not reactive. Changing them will not trigger a re-render, and they reset every time the component function runs again. useState is managed by React internally, so when we call the setter, React updates the state and re-renders the component automatically.
useEffect 的作用是什么?它和直接把代码写在组件函数体里面有什么本质区别?问题:
例如:为什么下面这种写法通常有问题?
xxxxxxxxxx41function User() {2 fetch('/api/user');3 return <div>User</div>;4}useEffect 的核心作用是:在 React 渲染完成后执行副作用代码,用于同步组件和 React 外部系统。外部系统包括:
因为组件函数体的本质是: Render 阶段(渲染阶段)
React 要求 render 必须:
错误示例:
xxxxxxxxxx41function User() {2 fetch('/api/user');3 return <div>User</div>;4}这段代码的问题在于:每次组件重新渲染都会重新请求。
都会导致重复请求,甚至死循环。
正确写法:
xxxxxxxxxx31useEffect(() => {2 fetch('/api/user');3}, []);This ensures the effect runs after render and only once on mount.
[] 的含义:只在首次挂载后执行一次。
本质区别
组件函数体:“描述 UI 应该长什么样”
useEffect:“UI 渲染后,需要顺便做什么事”
一句话总结:Render 用来计算界面,Effect 用来处理副作用。
Standard Answer (English)
useEffect is used to run side effects after React finishes rendering.Examples:
Because the component body runs during the render phase, which should stay pure.
React may re-run the component many times, so this:
xxxxxxxxxx11fetch('/api/user');could trigger repeated requests on every render.
面试官为什么问这个?
因为这题在考:
实战场景
常见错误:
xxxxxxxxxx31setInterval()2window.addEventListener()3fetch()直接写组件里,可能造成:
常见误区
错误:“useEffect = componentDidMount”
正确:useEffect 更像 “render 后同步副作用”,不仅仅是 mount。
面试口语版(中文)
useEffect 主要用于处理副作用,比如请求接口、事件监听或者定时器。因为组件函数本身是 render 阶段,应该保持纯净,如果直接把 fetch 写在函数体里,每次重新渲染都会执行,容易造成重复请求。useEffect 可以让这些操作在渲染完成后按依赖控制执行。
Interview Spoken Answer (English)
useEffect is mainly used for side effects like API calls, event listeners, or timers. The component body itself is the render phase and should stay pure. If we put something like fetch directly inside it, it may run on every render and cause repeated requests. useEffect lets us run those side effects after rendering and control them with dependencies.
useEffect 的依赖数组(dependency array)到底是做什么的?请你分别解释下面三种情况的区别,以及实际开发中各自适合什么场景:
xxxxxxxxxx31useEffect(() => {});2useEffect(() => {}, []);3useEffect(() => {}, [count]);useEffect 的第二个参数(依赖数组)本质上是: 告诉 React,当哪些依赖变化时,需要重新执行这个副作用。
React 会用浅比较(Object.is)检查依赖是否变化。
第一种:
xxxxxxxxxx11useEffect(() => {});含义:没有依赖数组 → 每次 render 后都执行
执行时机:
场景:很少用,因为容易造成性能问题。
风险:如果里面 setState,容易无限循环。
第二种:
xxxxxxxxxx11useEffect(() => {}, []);含义:空依赖 → 只在首次挂载后执行一次
场景:
注意:
如果引用外部变量,但不加依赖,可能拿到旧值(stale closure)。
第三种:
xxxxxxxxxx11useEffect(() => {}, [count]);含义:首次执行 + count 改变时重新执行
场景:
示例:
xxxxxxxxxx31useEffect(() => {2 fetchUser(count);3}, [count]);一句话总结
[]:只执行一次[x]:首次 + x 改变时执行更底层理解
不是“执行次数控制器”,而是:副作用和依赖之间的同步声明。
Standard Answer (English)
The dependency array tells React when the effect needs to re-run based on specific reactive values.
1.
xxxxxxxxxx11useEffect(() => {});Runs after every render.
Use case:
Rarely useful, can cause performance issues.
2.
xxxxxxxxxx11useEffect(() => {}, []);Runs only once after initial mount.
Use case:
3.
xxxxxxxxxx11useEffect(() => {}, [count]);Runs on mount + whenever count changes.
Use case:
Syncing side effects with changing state.
面试官为什么问这个?
因为这题核心在考:
实战场景
搜索:
xxxxxxxxxx31useEffect(() => {2 search(keyword);3}, [keyword]);
监听:
xxxxxxxxxx41useEffect(() => {2 window.addEventListener();3 return () => window.removeEventListener();4}, []);常见误区
错误:“[] 就等于 componentDidMount”
正确:更像“首次同步”,不是类组件生命周期的简单复制。
错误:“依赖不写更省事”
正确:
如果不写依赖可能导致:
面试口语版(中文)
依赖数组本质上是告诉 React,这个 effect 依赖哪些值变化。不写依赖就是每次渲染后都执行;空数组表示只在首次挂载执行一次;而写 [count] 表示首次挂载时执行,并且 count 改变时重新执行。实际开发里最重要的是保证副作用和依赖同步,否则容易出现无限循环或者拿到旧数据。
Interview Spoken Answer (English)
The dependency array tells React which values this effect depends on. Without it, the effect runs after every render. With an empty array, it runs only once after mount. With [count], it runs on mount and whenever count changes. In real projects, it’s mainly about keeping side effects synchronized with state changes and avoiding stale data or infinite loops.
useEffect 里有时候需要返回一个 cleanup function(清理函数)?请你结合真实开发场景解释:如果不清理,可能会出现什么问题?
例如:
xxxxxxxxxx31useEffect(() => {2 window.addEventListener('resize', handleResize);3}, []);useEffect 返回 cleanup function 的核心作用是: 在副作用失效前,撤销之前注册的外部资源,避免重复订阅、资源残留和状态错误。
常见需要清理的场景
xxxxxxxxxx71useEffect(() => {2 window.addEventListener('resize', handleResize);34 return () => {5 window.removeEventListener('resize', handleResize);6 };7}, []);为什么必须清理?
如果组件卸载后不移除:
结果可能包括:
内存泄漏
无用引用无法被 GC 回收
重复执行
一个 resize 触发多次 callback
状态更新到已卸载组件
可能出现:“Can't perform a React state update on an unmounted component”
xxxxxxxxxx51useEffect(() => {2 const timer = setInterval();34 return () => clearInterval(timer);5}, []);xxxxxxxxxx11return () => socket.close();xxxxxxxxxx11return () => controller.abort();
执行时机
Cleanup 会在两种情况下执行:
组件卸载时
下一次 effect 执行前(依赖变化)
示例:
xxxxxxxxxx51useEffect(() => {2 subscribe(id);34 return () => unsubscribe(id);5}, [id]);当 id 改变时:先取消旧订阅,再创建新订阅。
一句话总结:cleanup 的本质不是“清理代码”,而是撤销上一次副作用。
Standard Answer (English)
The cleanup function is used to remove or cancel side effects before they become invalid.
Its purpose is to:
Example:
xxxxxxxxxx71useEffect(() => {2 window.addEventListener('resize', handleResize);34 return () => {5 window.removeEventListener('resize', handleResize);6 };7}, []);Without cleanup:
Cleanup runs when:
面试官为什么问这个?
因为这是在考:
实战场景(非常常见)
错误:
xxxxxxxxxx51useEffect(() => {2 setInterval(() => {3 console.log('tick');4 }, 1000);5}, []);组件卸载后 interval 还在跑。
正确的写法,需要加上清理函数:
xxxxxxxxxx11return () => clearInterval(timer);
常见误区
错误:“只有卸载时才 cleanup”
正确:依赖变化时也会 cleanup 上一次 effect。
错误:“fetch 不需要 cleanup”
正确:异步请求也可能需要 abort,避免竞态问题。
面试口语版(中文)
cleanup function 的作用是撤销上一次副作用,比如移除事件监听、清除定时器、关闭订阅。如果不清理,组件卸载后这些外部资源可能还继续运行,导致内存泄漏、重复执行,甚至更新已经卸载的组件。它本质上是保证副作用生命周期正确。
Interview Spoken Answer (English)
The cleanup function is used to undo previous side effects, such as removing event listeners, clearing timers, or unsubscribing from external resources. Without cleanup, those side effects may continue after the component unmounts, causing memory leaks, duplicate execution, or stale state updates. Its main purpose is to keep side effects properly synchronized with the component lifecycle.
useRef 和 useState 都可以“保存值”,那它们的本质区别是什么?请你结合真实开发场景回答:
useRef 而不是 useState?ref.current 不会触发重新渲染?useRef 除了操作 DOM,还有哪些常见用途?useRef 和 useState 都能跨 render 保存值,但本质区别是:
useState用于影响 UI 的响应式状态
xxxxxxxxxx11const [count, setCount] = useState(0);特点:
useRef
用于保存不会影响 UI 的可变值
xxxxxxxxxx21const countRef = useRef(0);2countRef.current++;特点:
.current 不会 rerender
为什么 ref.current 不触发更新?
因为 React 的更新机制只监听:
ref 本质更像:一个持久化的普通对象容器
xxxxxxxxxx11{ current: value }改它不会进入 React 调度流程。
useRef的实战场景
场景1:操作 DOM
xxxxxxxxxx11inputRef.current.focus();场景2:保存定时器 ID
xxxxxxxxxx11const timerRef = useRef(null);避免 rerender。
场景3:保存上一次值
xxxxxxxxxx11prevValueRef.current = value;场景4:避免 stale closure
保存最新 callback。
什么时候选 useRef 而不是 useState?
用 useRef:
用 useState:
一句话总结useState 管 UI,useRef 管引用。
Standard Answer (English)
Both useState and useRef persist values across renders, but:
useState
Used for reactive state that affects UI.
useRef
Used for mutable values that should persist but not trigger UI updates.
ref.current does not re-render
Why?
Because useRef is just a persistent object:
xxxxxxxxxx11{ current: ... }
React doesn’t schedule updates for it.
useRef common real-world uses:
DOM access
Timer IDs
Previous values
External instances
Avoiding stale closures
面试官为什么问这个?
因为这题考你:
常见误区
错误:“useRef 就是拿 DOM 的”
正确:DOM 只是其中一个用途,它更常用于持久化非 UI 数据。
错误:“useRef 不能改”
正确:可以改,但 React 不关心。
面试口语版(中文)
useState 和 useRef 都可以跨渲染保存值,但 useState 是响应式状态,更新后会触发组件重新渲染,适合管理 UI。useRef 更像一个持久化容器,修改 .current 不会触发渲染,所以更适合保存 DOM、定时器、上一次值或者其他不影响界面的数据。
Interview Spoken Answer (English)
Both useState and useRef can persist values across renders, but useState is reactive and triggers re-render when updated, so it’s used for UI state. useRef is more like a persistent container whose .current value can change without causing re-render, so it’s useful for DOM references, timers, previous values, or other mutable data that doesn’t directly affect the UI.
useMemo 和 useCallback 的区别是什么?它们分别解决什么问题?请你结合实际开发说明:
一、核心区别(一句话)
useMemo:缓存计算结果(值)useCallback:缓存函数本身useMemo:缓存“值”
xxxxxxxxxx31const result = useMemo(() => {2 return computeExpensiveValue(count);3}, [count]);👉 返回的是“计算后的结果”
useCallback:缓存“函数”
xxxxxxxxxx31const handleClick = useCallback(() => {2 console.log(count);3}, [count]);👉 返回的是“函数引用本身”
👉 useCallback 其实是 useMemo 的语法糖
xxxxxxxxxx31useCallback(fn, deps)2// 等价于3useMemo(() => fn, deps)
React 默认行为:
每次 render:
问题1:昂贵计算重复执行
xxxxxxxxxx11const value = heavyCompute(data);👉 每次 render 都算一次(浪费性能)
问题2:子组件不必要 rerender
xxxxxxxxxx11<Child onClick={() => doSomething()} />👉 每次 render 都生成新函数 👉 子组件即使 memo 也会重新渲染
| Hook | 解决的问题 |
|---|---|
| useMemo | 避免重复计算 |
| useCallback | 避免函数引用变化导致子组件 rerender |
这是面试重点(很多人答不出来)
❌ 情况1:计算很简单
xxxxxxxxxx11const sum = useMemo(() => a + b, [a, b]);👉 useMemo 本身有缓存开销 👉 比直接计算更慢
❌ 情况2:子组件没优化
xxxxxxxxxx11const handle = useCallback(() => {}, []);但 Child 没有 memo:
👉 用不用 useCallback 都一样 rerender
❌ 情况3:依赖变化频繁
如果 deps 每次都变:等于“白缓存”
useMemo
useCallback
一句话总结
👉 useMemo 优化“计算”
👉 useCallback 优化“函数引用”
Standard Answer (English)
Core difference:
useMemo caches a computed valueuseCallback caches a functionuseMemo:
xxxxxxxxxx11const value = useMemo(() => compute(), [deps]);Returns a memoized value.
useCallback:
xxxxxxxxxx11const fn = useCallback(() => {}, [deps]);Returns a memoized function reference.
Because by default:
面试口语版(中文)
useMemo 是用来缓存计算结果的,避免每次 render 都做重复计算,比如排序或者过滤列表。useCallback 是用来缓存函数本身,避免函数引用变化导致子组件不必要的重新渲染。两者本质区别是一个缓存值,一个缓存函数。但如果计算很简单或者子组件没有做 memo 优化,用它们反而可能增加性能开销。
Interview Spoken Answer (English)
useMemo is used to memoize computed values to avoid expensive recalculations on every render. useCallback is used to memoize function references so that child components don’t re-render unnecessarily due to changed props. However, if the computation is trivial(琐碎的,不重要的) or the child component is not optimized with memo, using them can actually hurt performance instead of improving it.
useEffect 依赖项里如果写错了,会出现什么问题?请结合实际开发回答:
一、依赖写少了会怎样?
👉 会出现“数据过期 / 闭包旧值问题(stale closure)”
例子:
xxxxxxxxxx31useEffect(() => {2 console.log(count);3}, []);问题:
count 永远是“初始值”结果:
👉 UI 和逻辑不同步 👉 使用旧数据
二、依赖写多了会怎样?
👉 effect 频繁执行,引发性能问题甚至死循环
例子:
xxxxxxxxxx31useEffect(() => {2 fetchData();3}, [data]);问题:
结果:
👉 无限循环请求 / 多次重复执行
三、依赖写错的本质问题
👉 React 无法“自动猜测你依赖什么”
因为React 的设计原则是:
side effect must be explicit(清楚明白的,明确的)
四、为什么 React 不帮你自动处理依赖?
核心原因有三个:
xxxxxxxxxx11const x = props[a ? b : c];👉 React 无法静态分析真实依赖
effect 捕获的是“某一轮 render 的变量”
如果 React 自动追踪: 👉 会变成“全量依赖追踪系统” 👉 性能不可接受
五、真实开发总结
❌ 依赖少会造成:
❌ 依赖多会造成:
✅ 正确做法:
useRef / useCallback 优化一句话总结:dependency 写错 = React 逻辑同步失败
Standard Answer (English)
Leads to stale closure issues:
Leads to:
Because:
面试口语版(中文)
如果 useEffect 依赖写少了,就会出现闭包拿到旧值的问题,导致数据不同步。如果写多了,会导致 effect 频繁执行,甚至可能引发无限循环。React 不帮我们自动管理依赖,是因为 JavaScript 是动态的,React 无法准确判断你真正依赖哪些变量,所以必须开发者自己显式声明。
Interview Spoken Answer (English)
If dependencies are missing, we may get stale closure issues where the effect uses outdated values. If we include too many dependencies, the effect may run too often and even cause infinite loops or performance issues. React doesn’t automatically manage dependencies because JavaScript is dynamic and closures make it hard to statically determine what values are truly used, so developers must explicitly declare them.
useEffect 和 useLayoutEffect 有什么区别?在真实项目中你会怎么选择?请重点回答这几个点:
useLayoutEffect?useEffect
👉 不会阻塞页面显示
useLayoutEffect
在 DOM 更新完成(commit phase)后,浏览器绘制之前同步执行
👉 会阻塞浏览器绘制
| Hook | 是否阻塞渲染 |
|---|---|
| useEffect | ❌ 不阻塞 |
| useLayoutEffect | ✅ 阻塞 |
useEffect:
👉 “先把页面画出来,再处理副作用”
useLayoutEffect:
👉 “先处理完 DOM,再让浏览器画”
因为它是:
所以 React 必须等它执行完才能继续绘制
默认用 useEffect(99%场景)
适合:
必须用 useLayoutEffect 的场景
测量 DOM 尺寸
xxxxxxxxxx31useLayoutEffect(() => {2 const width = ref.current.getBoundingClientRect().width;3}, []);👉 防止闪烁
避免 UI 抖动(layout shift)
例如:
同步 DOM 修改
比如:
👉 useLayoutEffect = “防闪烁工具”
一句话总结
Standard Answer (English)
useEffect
Runs after the browser paints the screen (async).
useLayoutEffect
Runs after DOM updates but before paint (sync).
| Hook | Blocks paint |
|---|---|
| useEffect | No |
| useLayoutEffect | Yes |
面试口语版(中文)
useEffect 是在浏览器绘制完成之后执行的,不会阻塞页面渲染。而 useLayoutEffect 是在 DOM 更新完成后、浏览器绘制之前同步执行的,所以它会阻塞渲染。一般情况下用 useEffect,只有在需要读取 DOM 布局或者避免页面闪烁的时候才用 useLayoutEffect。
Interview Spoken Answer (English)
useEffect runs after the browser has painted the screen, so it doesn’t block rendering. useLayoutEffect runs synchronously after DOM updates but before painting, so it blocks the browser from rendering until it finishes. In most cases we use useEffect, and only use useLayoutEffect when we need to measure DOM or prevent visual flickering.
useContext 的作用是什么?它和“props 层层传递”有什么本质区别?请结合真实项目回答:
useContext 用于:跨层级共享数据,避免 props 层层传递(props drilling)
例如:
传统 props 传递问题:
xxxxxxxxxx11App → Page → Layout → Header → Avatar
中间组件只是“转发数据”,但不使用。
Context 方案:
xxxxxxxxxx21const UserContext = createContext();2const user = useContext(UserContext);👉 直接获取,不需要层层传递
所有消费该 Context 的组件都会重新渲染
这是面试重点 ⚠️
Context 本质是:“全局订阅源”。
一旦 value 变化:
useContext 的组件都会 rerender性能问题示例:
xxxxxxxxxx11<UserContext.Provider value={{ name }}>
每次 render 都创建新对象:所有子组件都会更新
因为它没有:
👉 所以它适合“低频变化数据”
❌ 高频更新数据
例如:
👉 会导致全局 rerender
❌ 大规模状态拆分
Context 会变成“全局 rerender 触发器”
✅ 适合:
❌ 不适合:
一句话总结: Context 用来“共享数据”,但不是“高性能状态管理工具”
Standard Answer (English)
useContext is used to avoid prop drilling by sharing data across deeply nested components.
When the context value changes, all components consuming that context re-render.
This is because they subscribe to the context provider.
Because it lacks fine-grained control:
面试口语版(中文)
useContext 的作用是解决 props drilling 问题,可以让组件跨层级直接获取数据,而不需要一层层传递。但它的问题是,一旦 context 的 value 发生变化,所有消费它的组件都会重新渲染,所以它不适合放高频更新的数据,一般用于用户信息、主题这些变化不频繁的全局状态。
Interview Spoken Answer (English)
useContext is used to solve prop drilling by allowing deeply nested components to access shared data directly. However, when the context value changes, all consuming components will re-render. Because of this, it is not suitable for frequently changing state, and is better used for global but stable data like user info, theme, or language settings.
useReducer 和 useState 有什么区别?在什么情况下你会选择 useReducer 而不是 useState?请重点回答:
useReducer 解决了 useState 的什么问题useState
👉 “状态 = 直接赋值更新”
xxxxxxxxxx11setCount(count + 1);useReducer
👉 “状态 = action 驱动 + reducer 计算”
xxxxxxxxxx31const [state, dispatch] = useReducer(reducer, initialState);23dispatch({ type: 'increment' });本质区别一句话
问题1:状态逻辑分散
xxxxxxxxxx31setA(...)2setB(...)3setC(...)
👉 逻辑散落在组件各处
useReducer 解决:
👉 所有状态变化集中在 reducer
xxxxxxxxxx61function reducer(state, action) {2 switch(action.type) {3 case 'updateA':4 case 'updateB':5 }6}
问题2:复杂状态更新难维护
例如:
问题3:难以追踪状态变化来源
useState: 👉 “哪里都可以改”
useReducer: 👉 “只能 dispatch”
✅ 适合场景
xxxxxxxxxx51formState = {2 name,3 email,4 password5}多状态联动
状态逻辑复杂
比如:
状态变化需要可追踪
👉 类似 Redux 思维
👉 useState = “简单状态” 👉 useReducer = “状态逻辑管理”
一句话总结:当状态变化“有规则、有结构、有多个触发方式”,就用 useReducer
Standard Answer (English)
useState
Direct state updates:
useReducer
Action-driven state management:
面试口语版(中文)
useState 适合简单状态,比如 input 或 toggle,而 useReducer 更适合复杂状态管理,因为它把所有状态更新逻辑集中在 reducer 里,通过 dispatch 来触发变化,这样状态变化更可控、更清晰,也更容易维护,尤其是在表单或者多状态联动的场景下。
Interview Spoken Answer (English)
useState is suitable for simple state like toggles or input values. useReducer is better for complex state management because it centralizes all state update logic inside a reducer function and uses dispatch to trigger changes. This makes state transitions more predictable and easier to maintain, especially in complex forms or multi-step workflows.
useMemo、useCallback 和 React.memo 三者在性能优化中分别扮演什么角色?它们之间有什么关系?请重点回答:
useMemo
👉 优化 “计算结果(值)”
xxxxxxxxxx31const filteredList = useMemo(() => {2 return list.filter(item => item.active);3}, [list]);
useCallback
👉 优化 “函数引用”
xxxxxxxxxx31const handleClick = useCallback(() => {2 console.log(count);3}, [count]);
React.memo
👉 优化 “组件重渲染”
xxxxxxxxxx31const Child = React.memo(function Child(props) {2 return <div>{props.value}</div>;3});👉 它们是层层配合的
场景:
xxxxxxxxxx11<Child onClick={() => doSomething()} value={data} />问题:
正确优化组合:
x1const handleClick = useCallback(() => {}, []);23const data = useMemo(() => compute(dataSource), []);45<Child onClick={handleClick} value={data} />6const Child = React.memo()❌ 只用 React.memo
没用 useCallback → props 仍然变 → memo 失效
❌ 只用 useCallback
Child 没 memo → 还是 rerender
❌ 只用 useMemo
函数还是重新创建 → props 仍变化
👉 结论: 它们必须组合使用才有意义
React rerender 来源有三个:
👉 这三者优化的就是:
| 工具 | 控制什么 |
|---|---|
| useMemo | 值 |
| useCallback | 函数引用 |
| React.memo | 组件是否重新渲染 |
结论:不是所有地方都需要优化
一句话总结:React.memo 控制“组件”,useMemo 控制“值”,useCallback 控制“函数”
Standard Answer (English)
useMemo: Optimizes computed values.
useCallback: Optimizes function references.
React.memo: Prevents unnecessary component re-renders.
They work together:
Because:
All three are interconnected.
面试口语版(中文)
useMemo 用来缓存计算结果,useCallback 用来缓存函数引用,React.memo 用来防止组件在 props 没变化时重复渲染。它们是配合使用的,如果只用其中一个,往往优化是无效的,比如函数没用 useCallback,React.memo 也会失效。
Interview Spoken Answer (English)
useMemo is used to memoize computed values, useCallback is used to memoize function references, and React.memo prevents unnecessary re-renders of components when props do not change. They are meant to work together; using only one of them often does not provide real performance benefits because the others can still cause re-renders.
useRef 在解决“闭包陷阱(stale closure)”问题时是怎么用的?请你结合真实场景说明:
useEffect + setInterval 容易出现这个问题useRef 是如何解决它的(核心机制)简单理解: 一个函数“记住”了它被定义时的变量值,即使这些变量在后续的组件渲染中已经发生了变化,该函数内部依然在使用那个旧的值。
示例:
xxxxxxxxxx221import React, { useState, useEffect } from 'react';23function Counter() {4 const [count, setCount] = useState(0);56 useEffect(() => {7 const timer = setInterval(() => {8 // 这里的 count 形成了闭包,它“锁死”在了这个 Effect 第一次执行时的值(即 0)9 console.log(`Current count: ${count}`); 10 }, 1000);1112 return () => clearInterval(timer);13 }, []); // ⚠️ 注意:依赖数组为空,Effect 只有在挂载时运行一次1415 return (16 <div>17 <p>Count: {count}</p>18 <button onClick={() => setCount(count + 1)}>增加 Count</button>19 </div>20 );21}22为什么会出现 Stale Closure?
①首次渲染:count 为 0。useEffect 运行,创建了一个 setInterval 回调函数。
②闭包形成:这个回调函数捕获了当前渲染周期的 count 变量(其值为 0)。
③点击按钮:你点击了多次按钮,count 在 React 内部已经变成了 1, 2, 3...,组件也重新渲染了。
④陷阱发生:但由于 useEffect 的依赖数组是 [],它不会重新运行,因此 setInterval 里持有的依然是第一次渲染时的函数引用。那个函数闭包里的 count 永远是 0。
51useEffect(() => {2 const id = setInterval(() => {3 console.log(count);4 }, 1000);5}, []);问题原因:
useEffect([]) 只执行一次count 是“初始 render 的值”👉 结果:count 永远不更新(或者永远是 0)
核心思路:
👉 ref 不会因为 re-render 重新创建
👉 .current 始终是最新值
正确写法:
131const countRef = useRef(0);23useEffect(() => {4 const id = setInterval(() => {5 console.log(countRef.current);6 }, 1000);78 return () => clearInterval(id);9}, []);1011useEffect(() => {12 countRef.current = count;13}, [count]);useRef 特点:
👉 所以: setInterval 读取的是:
xxxxxxxxxx11countRef.current // 永远最新
而不是闭包里的旧 count
stale closure 本质:
👉 “闭包 + React render 生命周期不一致”
useRef 的解决方案:
👉 “绕过闭包,改成引用读取最新值”
常见 bug:
一句话总结
👉 stale closure = “函数记住了旧 state” 👉 useRef = “用引用跳过闭包问题”
Standard Answer (English)
A stale closure happens when a function “captures” an old state value from a previous render and keeps using it even after the state has changed.
Because:
useEffect(() => {}, []) runs only onceuseRef provides a persistent object whose .current always holds the latest value without triggering re-renders.
So instead of relying on closure values, we read:
xxxxxxxxxx11ref.current
which is always up to date.
面试口语版(中文)
stale closure 的问题是函数会记住某一轮 render 的 state,所以在 setInterval 或异步回调里容易拿到旧值。useRef 可以解决这个问题,因为 ref 是一个一直存在的对象,它的 current 会保存最新值,不会受闭包影响,所以可以在定时器里直接读取最新状态。
Interview Spoken Answer (English)
A stale closure happens when a function captures state from an earlier render and keeps using that outdated value. This often occurs with setInterval or async callbacks inside useEffect. useRef solves this because it holds a persistent object whose .current always reflects the latest value, allowing us to access up-to-date state without relying on closures.
useTransition 是用来解决什么问题的?它和普通的 setState 有什么区别?请你尽量从真实交互体验角度来解释,比如:
useTransition 是如何改善体验的👉 解决“非紧急更新导致 UI 卡顿”的问题
在 React 18 之前,所有 state 更新:
例如:
21setInput(value);2setList(hugeFilteredList);问题:
👉 React 会一起执行 👉 UI 会卡顿(输入延迟)
👉 把更新分成两类:
| 类型 | 优先级 |
|---|---|
| 输入 / 点击 | 高优先级 |
| 列表渲染 | 低优先级(transition) |
51const [isPending, startTransition] = useTransition();23startTransition(() => {4 setList(filteredData);5});setState:立即执行,高优先级
useTransition:标记为“可中断的低优先级更新”
React 可以:
👉 提升“交互优先级”
用户感觉:
11isPending === true👉 表示“后台更新还没完成”
常用于:
👉 React 18 引入的是:“并发渲染(Concurrent Rendering)思想”
useTransition 是:👉 “让 React 有选择地延迟非关键更新”
一句话总结:useTransition = “让重要更新先执行,让不重要更新慢慢来”
Standard Answer (English)
It solves UI blocking caused by expensive or non-urgent state updates.
In traditional React:
It splits updates into:
Non-urgent updates are marked as transitions and can be interrupted.
Improves perceived performance and keeps UI responsive.
面试口语版(中文)
useTransition 主要解决的是 UI 卡顿问题,比如输入框更新很快,但后面还有一个很重的列表过滤,如果用普通 setState 会一起卡住 UI。而 useTransition 可以把这个更新标记为低优先级,让 React 先处理用户输入,再慢慢更新列表,从而提升交互流畅度。
Interview Spoken Answer (English)
useTransition is used to solve UI blocking issues caused by heavy state updates. In traditional React, all updates have the same priority, so expensive rendering can block user input. useTransition allows us to mark some updates as low priority, so React can prioritize urgent interactions like typing or clicking, and handle heavy updates in the background, improving overall responsiveness.
useSyncExternalStore 是干什么用的?它解决了什么问题?在什么场景下你会用到它?请重点从这几个角度回答:
👉 React 之外的状态源
也就是:不由 React 管理,但 UI 需要读取的数据。
例如:
👉 React 18 并发模式下的“数据不一致问题(tearing)”
什么是 tearing?
简单理解:👉 UI 在不同时间读取到了“不一致的状态版本”
例如:
👉 页面显示不一致
useState 问题:
useEffect 问题:
👉 在 concurrent rendering 下: useEffect 太晚了
👉 让 React 安全订阅外部 store
它保证:
41const state = useSyncExternalStore(2 subscribe,3 getSnapshot4);参数解释:
subscribe:监听外部变化
getSnapshot:获取当前状态
Redux(经典)
Redux 已经内部用它了:
11useSyncExternalStore(store.subscribe, store.getState);浏览器 API
window size
71useSyncExternalStore(2 (cb) => {3 window.addEventListener("resize", cb);4 return () => window.removeEventListener("resize", cb);5 },6 () => window.innerWidth7);Zustand(底层)
也是基于它实现订阅模型
👉 它不是“状态 hook”
👉 它是:“React 和外部系统的同步桥梁”
因为 concurrent rendering:
👉 需要一个“强一致性订阅机制”
一句话总结:useSyncExternalStore = “安全连接 React 和外部状态系统的标准接口”
Standard Answer (English)
Any state source outside React, such as:
It solves state tearing issues in React 18 concurrent rendering, where different renders may see inconsistent snapshots of external state.
It provides a safe way to subscribe to external stores while ensuring consistent snapshots during rendering.
面试口语版(中文)
useSyncExternalStore 是 React 用来连接外部状态系统的 hook,比如 Redux、Zustand 或浏览器 API。它解决的问题是在 React 18 并发渲染下,外部状态可能在不同组件中读取不一致的问题。它通过订阅机制保证每次渲染拿到的是一致的状态快照,避免数据不一致。
Interview Spoken Answer (English)
useSyncExternalStore is a hook used to safely connect React with external state sources like Redux, Zustand, or browser APIs. It solves the problem of state tearing in React 18 concurrent rendering, where different components might read inconsistent snapshots of external state. It ensures that React always reads a consistent snapshot during rendering through a subscription-based mechanism.
useId 是做什么用的?它和自己手写 Math.random() 或递增 id 有什么区别?在 SSR(服务端渲染)场景下它解决了什么问题?useId 用来生成稳定、唯一的 ID
常见用途:
示例:
41const id = useId();23<label htmlFor={id}>Name</label>4<input id={id} />❌ Math.random()
xxxxxxxxxx11const id = Math.random();
问题:
❌ 自增 id
xxxxxxxxxx11let id = 0;
问题:
✅ useId
特点:
解决 Hydration mismatch(服务端与客户端 DOM 不一致)
什么是 hydration mismatch?
SSR:
11<input id="a1" />CSR:
11<input id="b2" />👉 React 发现 DOM 不一致 → 报错或重新渲染
因为:
👉 React 在 服务端 + 客户端使用同一套 ID 生成规则
useId 的本质不是“随机数生成器”,而是:“跨 SSR/CSR 的稳定标识系统”。
表单绑定
xxxxxxxxxx21<label htmlFor={id}>2<input id={id} />ARIA accessibility
xxxxxxxxxx11aria-describedby={id}SSR 项目(Next.js)
避免 hydration warning
一句话总结
👉 useId = “保证 SSR + CSR 一致的稳定唯一 ID”
Standard Answer (English)
useId generates a stable and unique ID that remains consistent across renders and between server and client.
It solves hydration mismatch, where server-rendered HTML and client-rendered HTML have different IDs.
Because React generates IDs based on the component tree structure, ensuring consistency between server and client.
面试口语版(中文)
useId 用来生成稳定唯一的 ID,主要用于表单和无障碍场景。和 Math.random 或自增 ID 不同,它不会在每次 render 改变,而且在 SSR 场景下服务端和客户端生成的 ID 是一致的,可以避免 hydration mismatch 问题。
Interview Spoken Answer (English)
useId is used to generate stable and unique IDs, mainly for form associations and accessibility. Unlike Math.random or manual counters, it produces consistent IDs across renders and also ensures server and client output match in SSR, preventing hydration mismatches.
useImperativeHandle 是做什么用的?它和 forwardRef 是什么关系?在真实项目中你会在什么场景用它?请重点回答:
👉 用来自定义子组件通过 ref 暴露给父组件的方法或属性
正常情况下:
xxxxxxxxxx11ref.current
👉 直接拿到的是 DOM 或组件实例(有限)
但有时候你不想暴露整个 DOM 或组件内部结构。
👉 控制“子组件对外暴露的 API”
也就是:
不让父组件随便操作内部,只暴露必要方法
forwardRef:👉 让函数组件“能接收 ref”
useImperativeHandle:👉 定义“ref 到底暴露什么”
例子:父组件需要控制子组件里的 Input 焦点或弹窗(Modal)的开关,其余的方法不需要暴露给父组件。
x
1import React, { useState, useImperativeHandle, forwardRef, useRef } from 'react';23// 1. 使用 forwardRef 包裹组件,获取父组件传入的 ref4const SearchInput = forwardRef((props, ref) => {5 const inputRef = useRef();67 // 2. 使用 useImperativeHandle 定义暴露给父组件的对象8 useImperativeHandle(ref, () => {9 return {10 // 暴露一个聚焦方法11 focus: () => {12 inputRef.current.focus();13 },14 // 暴露一个清空内容的方法15 clear: () => {16 inputRef.current.value = '';17 }18 };19 }, []); // 依赖项通常为空,除非暴露的方法依赖某些 props 或 state2021 return <input type="text" ref={inputRef} placeholder="搜索点什么..." />;22});2324// 父组件25export default function Parent() {26 const searchRef = useRef(null);2728 return (29 <div style={{ padding: '20px' }}>30 <SearchInput ref={searchRef} />31 32 <div style={{ marginTop: '10px' }}>33 {/* 3. 父组件通过 ref 调用子组件暴露的方法 */}34 <button onClick={() => searchRef.current.focus()}>聚焦输入框</button>35 <button onClick={() => searchRef.current.clear()}>清空输入框</button>36 </div>37 </div>38 );39}40为什么要这么做?(对比直接使用 forwardRef)
useImperativeHandle:
父组件通过 ref 拿到的是整个 <input /> DOM 节点。这意味着父组件可以修改子组件的样式、删除它、或者做任何破坏性的操作,这破坏了子组件的封装性。focus() 和 clear()”。父组件拿不到底层的 DOM,只能按规矩办事。如果不用它:
xxxxxxxxxx11ref.current = <整个组件实例/DOM>
问题:
👉 React 推荐: “只暴露必要 API,而不是整个内部结构”
场景1:表单组件(非常常见)
31childRef.current.submit();2childRef.current.reset();3childRef.current.validate();场景2:弹窗控制
21modalRef.current.open();2modalRef.current.close();场景3:输入框聚焦
11inputRef.current.focus();但可以只暴露 focus,而不暴露 DOM 全部能力
👉 它不是“操作 ref 的 hook”
👉 而是:“组件对外 API 封装机制”
注意事项
useImperativeHandle 暴露的方法中使用了某些 State,记得在依赖数组(第三个参数)中添加对应的 State,否则父组件调用的方法可能会遇到陈旧闭包问题,拿到的是旧的状态。一句话总结:useImperativeHandle = “控制 ref 暴露范围,让组件像 API 一样被调用”
Standard Answer (English)
It allows you to customize what a parent component can access through a ref.
Without it, a ref exposes the entire DOM or component instance, which breaks encapsulation.
面试口语版(中文)
useImperativeHandle 是用来控制父组件通过 ref 能访问子组件哪些方法的。它通常和 forwardRef 一起使用,forwardRef 是让函数组件能接收 ref,而 useImperativeHandle 是用来定制 ref 暴露的内容,比如只暴露 open、close 或 focus 方法,而不是整个 DOM 或组件实例,这样可以更好地封装组件。
Interview Spoken Answer (English)
useImperativeHandle is used to control what a parent component can access through a ref. It works together with forwardRef, where forwardRef allows a functional component to receive a ref, and useImperativeHandle defines what methods or properties are exposed. This is useful for encapsulation, for example exposing only methods like open, close, or focus in modal or form components instead of exposing the entire internal instance.
请重点回答:
useState / useRef 的关系👉 表单数据由 React state 完全控制
31const [value, setValue] = useState("");23<input value={value} onChange={(e) => setValue(e.target.value)} />特点:
👉 表单数据由 DOM 自己管理
31const inputRef = useRef();23<input ref={inputRef} />获取值:
11inputRef.current.value;特点:
| 类型 | Hook |
|---|---|
| 受控组件 | useState |
| 非受控组件 | useRef |
| 对比 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据来源 | React state | DOM |
| 更新方式 | onChange → setState | DOM 自己更新 |
| 可控性 | 高 | 低 |
| 性能 | 可能稍差 | 更快 |
受控组件适合:
表单校验
xxxxxxxxxx11if (value.length < 6) 实时联动
可预测状态
非受控组件适合:
简单表单
性能敏感场景
与第三方库集成
(如 jQuery 插件)
👉 因为:
数据可预测
state = 唯一数据源
更容易调试
所有变化都在 React 里
更容易做联动逻辑
但缺点是:
一句话总结
👉 受控组件 = React 管数据 👉 非受控组件 = DOM 管数据
Standard Answer (English)
A controlled component is where form data is fully managed by React state.
An uncontrolled component stores its state in the DOM, and React accesses it via ref when needed.
| Controlled | Uncontrolled |
|---|---|
| React state | DOM |
| onChange updates state | DOM handles updates |
| predictable | less controlled |
Controlled:
Uncontrolled:
Because it provides:
面试口语版(中文)
受控组件就是表单数据由 React 的 state 来控制,每次输入都会更新 state,从而驱动 UI。非受控组件则是数据存在 DOM 里,通过 ref 去读取。React 更推荐受控组件,因为数据流更清晰、可控,也更容易做校验和联动逻辑。
Interview Spoken Answer (English)
A controlled component is where the form state is managed by React state, so every input change updates the state and keeps UI in sync. An uncontrolled component stores its value in the DOM, and we access it via refs when needed. React prefers controlled components because they provide a predictable data flow, easier debugging, and better control over form logic and validation.
请重点回答:
👉 使用 浅比较(shallow comparison)
底层使用的是:Object.is。
示例:
11useEffect(() => {}, [obj]);React 做的是:
xxxxxxxxxx1Object.is(prevObj, nextObj)结论:
👉 比较的是引用是否变化(reference equality)
不是内容比较
这是面试重点 ⚠️
❌ 如果用深比较:
问题1:性能极差
xxxxxxxxxx11deepEqual(obj1, obj2)
对象可能很大:
👉 每次 render 都要遍历整个结构
问题2:无法预测成本
React render 应该是:
O(1) 或 O(n)
深比较可能变成:
O(n²) 或更糟
问题3:函数/循环引用复杂
👉 深比较非常不可靠
❗ 1. 对象/数组“误触发更新”
11useEffect(() => {}, [{ a: 1 }]);每次 render: 👉 都是新对象引用 👉 effect 每次都会执行
❗ 2. 函数依赖问题
11useEffect(() => {}, [() => {}]);👉 每次都是新函数引用
❗ 3. 容易导致无限循环
31useEffect(() => {2 setState({});3}, [obj]);✅ 1. useMemo(稳定对象)
31const obj = useMemo(() => ({2 a: 13}), []);✅ 2. useCallback(稳定函数)
11const fn = useCallback(() => {}, []);✅ 3. 拆分依赖(不要传大对象)
✅ 4. ESLint hooks plugin
自动提醒 missing deps
👉 React 不关心“内容是否相等”
👉 它只关心:“这一轮 render 的引用有没有变化”
一句话总结:deps 比较是 Object.is 的浅比较,本质是“引用是否变化”,不是内容比较。
Standard Answer (English)
React uses shallow comparison with Object.is, meaning it compares reference equality, not deep value equality.
useMemo for stable objectsuseCallback for stable functions面试口语版(中文)
React 在比较依赖项的时候用的是浅比较,也就是 Object.is,它只判断引用是否变化,而不是内容是否相等。因为如果做深比较性能会非常差,而且无法预测复杂数据结构的成本。所以如果在依赖里传对象或者函数,每次 render 都会产生新的引用,就会导致 effect 频繁触发,这时候一般会用 useMemo 或 useCallback 来保持引用稳定。
Interview Spoken Answer (English)
React uses shallow comparison with Object.is to check whether dependencies have changed, meaning it compares references instead of deep values. Deep comparison is not used because it would be too expensive and unpredictable for complex data structures. As a result, objects or functions created during render will often change references and retrigger effects, which is why we use useMemo and useCallback to stabilize them.
setState 之后不能立刻拿到最新值?请重点回答:
这里的“异步”不是 Promise / async...await,而是指:React 会把状态更新先加入队列(batching),不会立刻修改 state
示例:
21setCount(count + 1);2console.log(count);👉 log 还是旧值
因为 React 18做了一个优化:👉 批处理(Batching)
如果每次 setState 都立刻更新:
👉 性能爆炸
React 的做法:
👉 把多个 setState 合并成一次 render
简化理解:
xxxxxxxxxx11setState → 加入队列 → 等待批处理 → 统一 render → 更新 UI
❗ 1. 立即读取旧值
21setCount(count + 1);2console.log(count); // 旧值❗ 2. 依赖旧 state 更新错误
21setCount(count + 1);2setCount(count + 1);👉 可能只 +1
✔ 函数式更新
21setCount(prev => prev + 1);2setCount(prev => prev + 1);👉 才会 +2
方法1:useEffect
31useEffect(() => {2 console.log(count);3}, [count]);方法2:useRef(同步场景)
11ref.current = count;方法3:函数式 setState(依赖旧值)
👉 核心目标:
一句话总结:setState “异步”指的是 React 会延迟 + 合并更新,而不是立即修改 state
Standard Answer (English)
It does NOT mean Promise-based async. It means React batches state updates and applies them later during rendering.
setState(prev => ...)面试口语版(中文)
setState 的“异步”并不是 Promise 的异步,而是 React 会把多个状态更新先放到队列里统一处理,做批量更新来优化性能,所以在调用 setState 之后不能立刻拿到最新值。如果想基于最新状态更新,应该使用函数式 setState,或者通过 useEffect 来监听更新后的值。
Interview Spoken Answer (English)
The “async” nature of setState does not mean Promise-based async. It means React batches multiple state updates together and applies them during rendering for performance optimization. Because of this batching, we cannot immediately access the updated state after calling setState. To correctly update based on the latest state, we should use functional updates or rely on useEffect after state changes are committed.
请从更底层理解回答:
(这是一个偏原理 + 架构理解题)
👉 React 用的是:“调用顺序(call order index)”来绑定 Hook
本质机制:
每个组件在 render 时,React 会维护一个“Hook 指针”:
xxxxxxxxxx31第 1 次 useState → index 02第 2 次 useState → index 13第 3 次 useEffect → index 2
👉 Hook 和 state 是靠“顺序位置”绑定的,而不是名字。
❌ 错误示例:
51if (flag) {2 useState(1);3}45useState(2);问题:
不同 render 会变成:
render 1:
xxxxxxxxxx21useState(1) → index 02useState(2) → index 1
render 2(flag=false):
xxxxxxxxxx11useState(2) → index 0 ❌ 错位了
👉 React 无法知道:
👉 会破坏 React 的核心设计:Hook 的稳定映射关系(stable hook ordering)
结果:
这是这题的高分点 ⚠️
❌ 方案1:用变量名
11useState("count")问题:
❌ 方案2:用 key
11useState({ key: "count" })问题:
❌ 更关键问题:
React 每次 render 是“重新执行函数”,没有稳定身份系统。
因为:
✔ 简单
✔ O(1) 访问
✔ 不需要额外结构
✔ 适配函数组件执行模型
👉 React 选择了最朴素但最稳定的方案:
“用执行顺序作为唯一标识”
在开发模式:
👉 React 会记录 Hook 调用顺序
如果发现:
👉 直接报错:
xxxxxxxxxx11Hooks called in a different order
一句话总结
👉 Hooks 依赖“调用顺序”来绑定 state,因为 React 没有办法在函数执行中稳定识别变量身份
Standard Answer (English)
React uses call order indexing to associate hooks with state.
Each render maintains a pointer that maps:
Hook order changes between renders, causing:
Because:
Because it is:
面试口语版(中文)
React 之所以不能在条件语句里调用 Hooks,是因为它是通过“调用顺序”来区分每个 Hook 对应的 state 的,而不是通过变量名或者 key。如果在 if 或循环里调用 Hook,会导致不同 render 的调用顺序不一致,从而造成 state 错位。React 也没有办法在函数执行时稳定识别 Hook 的身份,所以只能依赖顺序这种最稳定的方式。
Interview Spoken Answer (English)
React cannot allow hooks inside conditions because it relies on the order of hook calls to associate state with each hook. If hooks are conditionally executed, the order will change between renders, causing state mismatches. React does not use variable names or keys because they are not reliable or stable during function execution, so the only consistent way is to use call order as the identifier.
use 开头?请重点回答:
use 开头(规则 + ESLint + React 机制)本质:封装可复用的 Hook 逻辑函数
31function useXXX() {2 // 可以使用 hooks3}它解决的问题:逻辑复用(logic reuse)
比如:
这是核心考点 ⚠️
普通函数:
31function utils() {2 useState(); // ❌ 不允许3}特点:
自定义 Hook:
31function useFetch() {2 const [data, setData] = useState();3}特点:
本质区别一句话:
👉 自定义 Hook = “带状态的逻辑复用单元”
这是面试重点 ⚠️
原因1:React Hook 规则识别
React 内部通过“函数名是否以 use 开头”,来判断这个函数是否可能包含 Hook。
原因2:ESLint 检查
xxxxxxxxxx11react-hooks/rules-of-hooks
会强制要求:
👉 useXXX 才能调用 Hook
原因3:防止错误使用
例如:
31function fetchData() {2 useState(); // ❌3}React 无法检测 → 会报错或逻辑错乱
因为:
👉 React 无法在运行时判断“普通函数里是否用了 Hook”
所以只能用:
命名约定 + lint 规则
场景1:请求封装
91function useUser(id) {2 const [user, setUser] = useState();34 useEffect(() => {5 fetchUser(id).then(setUser);6 }, [id]);78 return user;9}场景2:防抖
31function useDebounce(value, delay) {2 const [debounced, setDebounced] = useState(value);3}场景3:订阅事件
51function useOnlineStatus() {2 useEffect(() => {3 window.addEventListener()4 }, []);5}👉 自定义 Hook = “逻辑组件化”
不是 UI 组件,而是:state + effect + logic 的复用单元
一句话总结:自定义 Hook 是把 React 逻辑抽成可复用函数,但本质仍然依赖 Hook 体系运行
Standard Answer (English)
A custom Hook is a function that starts with use and allows reuse of React stateful logic.
Because:
面试口语版(中文)
自定义 Hook 本质是把 React 的状态逻辑和副作用逻辑抽成可复用的函数,它可以使用 useState 和 useEffect,而普通函数不行。必须以 use 开头是因为 React 和 ESLint 会通过这个规则来识别这个函数是否包含 Hook,从而保证 Hook 的调用规则不被破坏。常见场景包括请求封装、防抖、订阅事件等。
Interview Spoken Answer (English)
A custom Hook is a function that allows us to reuse React stateful logic using Hooks like useState and useEffect. Unlike normal functions, custom Hooks can participate in React’s lifecycle. They must start with "use" because React and ESLint rely on this naming convention to identify Hook usage and enforce the rules of Hooks, preventing incorrect usage. Common use cases include data fetching, debounce logic, and event subscriptions.
useEffect 来管理它,而不能直接在 render 里做?请重点回答:
👉 简单理解:所有“影响 React 之外的操作”都是副作用
常见副作用:
👉 React 的 render 应该是:纯函数(pure function)
规则:
理想模型:
xxxxxxxxxx11UI = f(state)
❌ 示例:
x
1function App() {2 fetch("/api"); // ❌3 return <div />;4}问题:
会重复执行
每次 render 都发请求
破坏纯函数原则
render 不再可预测
可能造成无限循环
xxxxxxxxxx11setState → render → fetch → setState → renderUI 和副作用混乱
难以控制执行时机
👉 React 把副作用放到:render 之后执行
生命周期顺序(简化):
xxxxxxxxxx11render → commit DOM → useEffect
为什么这样设计?
✔ 保证 UI 先稳定显示
✔ 不阻塞渲染
✔ 副作用可控执行
它不是“副作用工具”,而是“render 完成后的安全执行区”。
React 分成两部分:
| 阶段 | 职责 |
|---|---|
| render | 计算 UI |
| effect | 处理外部系统 |
👉 这是 React 的核心架构分离
一句话总结:副作用是“影响外部世界的操作”,必须放在 useEffect,因为 render 必须保持纯函数
Standard Answer (English)
A side effect is any operation that affects something outside React, such as:
The render phase must be pure:
UI = f(state)
It should not cause side effects or modify external systems.
useEffect runs after the render is committed to the DOM, ensuring:
面试口语版(中文)
副作用就是所有会影响 React 之外的操作,比如请求接口、操作 DOM 或定时器。React 的 render 阶段应该是纯函数,只负责根据 state 计算 UI,如果在 render 里写副作用,会导致每次渲染都重复执行,甚至可能造成死循环。所以 React 把副作用放在 useEffect 里,让它在 DOM 更新完成之后再执行,这样可以保证 UI 先稳定渲染,再处理外部逻辑。
Interview Spoken Answer (English)
A side effect is any operation that affects something outside of React, such as API calls, DOM manipulation, or timers. The render phase in React must remain pure and only calculate the UI based on state. If we put side effects inside render, it would cause repeated executions and unpredictable behavior. That’s why React uses useEffect, which runs after the DOM is updated, ensuring the UI is rendered first before handling external operations.
一、标准答案(中文)
闭包陷阱指的是:在异步或延迟执行的函数中,拿到的是“旧的 state / props”,而不是最新值。
xxxxxxxxxx221import React, { useState, useEffect } from 'react';23function Counter() {4 const [count, setCount] = useState(0);56 useEffect(() => {7 const timer = setInterval(() => {8 // 这里的 count 形成了闭包,它“锁死”在了这个 Effect 第一次执行时的值(即 0)9 console.log(`Current count: ${count}`); 10 }, 1000);1112 return () => clearInterval(timer);13 }, []); // ⚠️ 注意:依赖数组为空,Effect 只有在挂载时运行一次1415 return (16 <div>17 <p>Count: {count}</p>18 <button onClick={() => setCount(count + 1)}>增加 Count</button>19 </div>20 );21}22二、为什么会发生?
核心原因:
👉 JavaScript 闭包机制,函数会“记住”它创建时的变量环境。
而 React 中:
三、如何解决闭包陷阱?
11setCount(prev => prev + 1)51const countRef = useRef(count)23useEffect(() => {4 countRef.current = count5}, [count])31useEffect(() => {2 console.log(count)3}, [count])五、面试核心总结一句话
“闭包陷阱的本质是:函数捕获的是创建时的 state,而不是运行时的 state。”
English Version (Interview Answer)
A stale closure happens when a function captures and uses outdated state or props instead of the latest values.
In async code like setTimeout:
31setTimeout(() => {2 console.log(count)3}, 1000)It logs the value of count at the time the function was created, not the latest value.
Because of JavaScript closures:
A closure always “remembers” the value from when it was created, not when it is executed.
中文口语版
闭包陷阱指的是,在异步函数或者定时器里面拿到的是旧的 state,而不是最新的值。
本质原因是 JavaScript 的闭包机制,每次 render 都会生成一份新的 state,但是异步函数捕获的是创建时那一份变量。
比如 setTimeout 或 useEffect 里面,如果依赖没写好,就会拿到旧值。
解决方式一般有三种:用函数式更新、用 useRef 保存最新值,或者把 state 放进依赖数组里。
English spoken version
A stale closure happens when an async function uses outdated state instead of the latest one.
The root cause is JavaScript closures. Each render creates a new scope, but async callbacks capture the variables from the time they were created.
So in cases like setTimeout or useEffect, you may see stale values.
To fix it, we can use functional updates, useRef for latest value, or properly manage dependencies.
一、标准答案(中文)
函数式更新指的是:
11setCount(prev => prev + 1)而不是:
11setCount(count + 1)二、两者的本质区别
❌ 非函数式写法:
11setCount(count + 1)👉 使用的是“当前闭包里的 count”
✅ 函数式写法:
11setCount(prev => prev + 1)👉 使用的是“React 最新的 state 值”
三、关键问题:为什么会出 bug?
React state 更新是异步 + 批处理(batching)的。
举例:
31setCount(count + 1)2setCount(count + 1)3setCount(count + 1)👉 你以为 +3 👉 实际可能只 +1
因为:这 3 次用的是同一个旧 count
四、正确写法(函数式更新)
31setCount(prev => prev + 1)2setCount(prev => prev + 1)3setCount(prev => prev + 1)👉 一定 +3
五、什么时候必须用函数式更新?
21setCount(c => c + 1)2setCount(c => c + 1)例如:
31setTimeout(() => {2 setCount(count + 1) // ❌ 可能是旧值3}, 1000)👉 应该:
31setTimeout(() => {2 setCount(prev => prev + 1)3}, 1000)
六、React 为什么要这样设计?
核心原因:
React state 更新是 基于队列(queue)批处理的,它不保证你拿到的是“最新 state”。
函数式更新提供:
✔ 可靠性 ✔ 避免闭包旧值问题 ✔ 支持并发渲染(concurrent rendering)
English Version (Interview Answer)
11setCount(prev => prev + 1)Instead of:
11setCount(count + 1)❌ Direct value update:
Uses the value from current closure.
✅ Functional update:
Uses the latest state value provided by React.
React state updates are batched and asynchronous.
So:
31setCount(count + 1)2setCount(count + 1)3setCount(count + 1)may only increase once.
11setCount(prev => prev + 1)Ensures each update is based on the latest state.
Because React batches updates and cannot guarantee immediate state consistency, functional updates ensure correctness and avoid stale closures.
中文口语版
函数式更新就是 setState 传一个函数,比如 setCount(prev => prev + 1)。
它和直接写 setCount(count + 1) 最大区别是,前者拿到的是 React 最新的 state,而后者用的是当前闭包里的旧值。
在 React 里 state 更新是异步并且会批处理的,所以如果你连续调用多次 setCount(count + 1),可能只会生效一次。
但是如果用函数式更新,每次都会基于最新值,所以可以保证结果正确。
一般在连续更新 state、依赖上一次 state 或者异步场景下,我都会用函数式更新。
English spoken version
Functional update means passing a function to setState, like setCount(prev => prev + 1).
The difference is that the normal way uses the value from the current closure, while functional update always uses the latest state provided by React.
Because state updates are asynchronous and batched, multiple updates using the same value may not work correctly.
So in cases like consecutive updates, async callbacks, or when the new state depends on the previous one, I always use functional updates to ensure correctness.
一、标准答案(中文)
React 组件发生 re-render 主要有 4 种情况:
(1)state 变化
useState / setState 更新会触发重新渲染(2)props 变化
(3)context 变化
useContext 依赖的 value 变化,会触发所有消费组件 re-render(4)父组件 re-render(间接触发)
二、关键面试点(容易加分)
❗ 父组件 re-render ≠ 子组件一定有变化
但: 👉 子组件函数一定会重新执行(默认情况下)
三、如何避免不必要的 re-render?
11export default React.memo(MyComponent)
四、常见面试坑
❌ 误区1:
“子组件 props 没变就不会 re-render”
👉 错,父组件 re-render ,默认子组件也会执行 render
❌ 误区2:
“useMemo / useCallback 可以阻止 re-render”
👉 错,它们只是优化引用,不是阻止 render 的核心手段
English Version (Interview Answer)
A component re-renders in these cases:
(1) State change
When useState or setState updates, React triggers a re-render.
(2) Props change
When parent re-renders and passes new props, child components re-render.
(3) Context change
When a value in useContext changes, all consuming components re-render.
(4) Parent re-render
Even if props don’t change, children still re-render by default.
A parent re-render does NOT guarantee meaningful changes in children, but it still causes child render execution.
中文口语版
React 组件重新渲染主要有几种情况。
第一是 state 变化,比如 useState 更新就会触发 re-render。
第二是 props 变化,父组件更新后子组件会重新渲染。
第三是 context 变化,useContext 依赖的值变了也会触发更新。
还有一个很重要的点是,即使 props 没变,只要父组件 re-render,子组件默认也会重新执行 render。
优化方面,我一般会用 React.memo 来避免不必要的渲染,用 useMemo 和 useCallback 来稳定引用,然后通过拆组件和拆 state 来减少影响范围。
English spoken version
A React component re-renders mainly in four cases.
First, when state changes, like useState updates.
Second, when props change from the parent component.
Third, when context value changes, all consuming components re-render.
And also, even if props don’t change, a child component will still re-render when its parent re-renders by default.
To optimize this, I usually use React.memo, useMemo and useCallback to stabilize references, and also split components and state to reduce unnecessary re-renders.
一、标准答案(中文)
因为:Hooks 必须在 React 组件的“渲染过程(render phase)”中执行。
而函数外:
所以会报错:Invalid hook call
二、核心本质:Hooks 依赖“当前组件实例”
React 内部维护了:👉 Fiber Node(组件实例结构)
每个组件在 React 内部都有一份:
三、Hooks 是怎么工作的?
当组件执行时:
31function Counter() {2 const [count, setCount] = useState(0)3}React 实际做的是:
四、为什么不能在函数外用?
函数外:
11const [count, setCount] = useState(0) // ❌问题是:
❌ 没有 fiber
❌ 没有 render 上下文
❌ 没有 hook 顺序链
❌ React 无法挂载状态
五、为什么 Hooks 必须“按顺序调用”?
React 用的是: 链表 + index 顺序匹配
例如:
31useState()2useEffect()3useMemo()React 不是用名字匹配,而是:
👉 第1个 hook、第2个 hook、第3个 hook
如果你放在 if 里:
31if (flag) {2 useState()3}👉 hook 顺序就乱了 👉 React 找不到正确 state
六、一句话总结(面试杀手级)
👉 Hooks 依赖的是 React 的“Fiber 渲染上下文 + 调用顺序机制”,脱离 render 环境就无法工作。
English Version (Interview Answer)
Because hooks must run inside React's render process.
Outside a component:
So React throws an error: invalid hook call.
React stores component state inside a structure called Fiber Node.
Each component instance has:
Hooks are matched by call order, not by name.
So React relies on consistent execution order during render.
Because there is:
So React cannot attach state anywhere.
Hooks only work inside React render because they depend on internal fiber and sequential execution order.
中文口语版
useState 不能在函数外使用的原因是,Hooks 必须运行在 React 的渲染流程里面。
React 内部是通过 Fiber 结构来管理每个组件的 state 的,每个组件都有自己的 hooks 链表,并且是按调用顺序来存储的。
如果你在函数外调用 useState,就没有组件实例,也没有 fiber,React 根本不知道这个 state 属于谁。
所以 Hooks 必须依赖组件的执行上下文,而且必须按顺序调用,否则 React 就会乱掉。
English spoken version
We can’t use useState outside a function component because hooks must run inside React’s render process.
React uses a Fiber structure to manage each component’s state, and hooks are stored in a list based on call order.
Outside a component, there is no fiber instance and no render context, so React has no way to track the state.
That’s why hooks must run inside components and follow a consistent call order.